package internal

import (
	"context"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"log/slog"
	"os"
	"strings"

	"github.com/samber/lo"
	"github.com/xuender/kit/los"
)

type Bind struct {
	pkgs map[string][]*ast.File
}

func NewBind() *Bind {
	return &Bind{
		pkgs: map[string][]*ast.File{},
	}
}

func (p *Bind) Add(path string) {
	if _, has := p.pkgs[path]; has {
		return
	}

	pkgs, errs := LoadPackages(context.Background(), los.Must(os.Getwd()), os.Environ(), "", []string{path})
	if len(errs) > 0 {
		los.Must0(errs[0])
	}

	fset := token.NewFileSet()
	files := []*ast.File{}

	for _, pkg := range pkgs {
		for _, file := range pkg.GoFiles {
			file := los.Must(parser.ParseFile(fset, file, nil, parser.ParseComments))
			files = append(files, file)
		}
	}

	p.pkgs[path] = files
}

func (p *Bind) Bind(igen *bindGen, target, source *types.Var, ignores []string) {
	slog.Info("bind",
		"target", target.Name(),
		"targetType", target.Type(),
		"source", source.Name(),
		"sourceType", source.Type(),
		"ignores", ignores,
	)

	idx := strings.LastIndex(target.Type().String(), ".")
	targetPkg := target.Type().String()[:idx]
	targetStruct := target.Type().String()[idx+1:]

	targetPkg = strings.Trim(targetPkg, "*")
	slog.Info("target", "pkg", targetPkg, "model", targetStruct)

	idx = strings.LastIndex(source.Type().String(), ".")
	sourcePkg := source.Type().String()[:idx]
	sourceStruct := source.Type().String()[idx+1:]

	sourcePkg = strings.Trim(sourcePkg, "*")
	slog.Info("source", "pkg", sourcePkg, "model", sourceStruct)

	p.Add(targetPkg)
	p.Add(sourcePkg)

	sourceFields := p.Geter(sourcePkg, sourceStruct)
	targetFields := p.Seter(targetPkg, targetStruct)

	for _, name := range targetFields.Mixed(sourceFields) {
		if lo.Contains(ignores, name) {
			continue
		}

		sourceField := sourceFields.Find(name)
		targetField := targetFields.Find(name)
		val := sourceField.Val(source.Name(), targetField.Type)

		if targetField.Mod == Set {
			igen.Pf("\t"+target.Name()+".Set"+name+"(%s)\n", val)
		} else {
			igen.Pf("\t"+target.Name()+"."+name+" = %s\n", val)
		}

		for key, val := range sourceField.Imports {
			if _, has := igen.g.imports[key]; !has {
				igen.g.imports[key] = val
			}
		}
	}
}

func (p *Bind) fields(pkgName, structName, prefix string) Fielders {
	pkg, has := p.pkgs[pkgName]
	if !has {
		return nil
	}

	ret := Fielders{}

	for _, file := range pkg {
		for _, decl := range file.Decls {
			genDecl, isOk := decl.(*ast.GenDecl)
			if !isOk || genDecl.Tok != token.TYPE {
				continue
			}
			// 遍历结构体定义
			for _, spec := range genDecl.Specs {
				typeSpec := spec.(*ast.TypeSpec)

				structType, isOk := typeSpec.Type.(*ast.StructType)
				if !isOk {
					continue
				}
				//
				if typeSpec.Name.Name != structName {
					continue
				}

				ret = readFields(structType, ret)

				ast.Inspect(file, func(node ast.Node) bool {
					ret = readFuncs(node, typeSpec, prefix, ret)

					return true
				})
			}
		}
	}

	return ret
}

func readFuncs(node ast.Node, typeSpec *ast.TypeSpec, prefix string, ret Fielders) Fielders {
	if fun, isOk := node.(*ast.FuncDecl); isOk {
		if fun.Recv == nil {
			return ret
		}

		if len(fun.Recv.List) == 0 {
			return ret
		}

		if ident, isOk := fun.Recv.List[0].Type.(*ast.StarExpr); isOk {
			if ident.X.(*ast.Ident).Name == typeSpec.Name.Name && strings.HasPrefix(fun.Name.Name, prefix) {
				mod := Get
				fieldType := ""

				if prefix == "Set" {
					mod = Set
					ident := fun.Type.Params.List[0].Type.(*ast.Ident)
					fieldType = ident.Name
				} else {
					// ident := fun.Type.Results.List[0].Type.(*ast.Ident)
					// fieldType = ident.Name
					ident, ok := fun.Type.Results.List[0].Type.(*ast.Ident)
					if ok {
						fieldType = ident.Name
					}
				}

				ret.Add(Fielder{
					Name:    fun.Name.Name[len(prefix):],
					Mod:     mod,
					Type:    fieldType,
					Imports: map[string]ImportInfo{},
				})
			}
		}
	}

	return ret
}

func readFields(structType *ast.StructType, ret Fielders) Fielders {
	for _, field := range structType.Fields.List {
		for _, name := range field.Names {
			if name.IsExported() {
				fieldType := ""

				switch obj := field.Type.(type) {
				case *ast.ArrayType, *ast.StarExpr:
					continue
				case *ast.Ident:
					fieldType = obj.Name
				case *ast.SelectorExpr:
					pkgIdent, isOk := obj.X.(*ast.Ident)
					if !isOk {
						slog.Warn("Unknown package")

						continue
					}

					fieldType = pkgIdent.Name + "." + obj.Sel.Name
				}

				ret.Add(Fielder{
					Name:    name.Name,
					Mod:     Exported,
					Type:    fieldType,
					Imports: map[string]ImportInfo{},
				})
			}
		}
	}

	return ret
}

func (p *Bind) Geter(pkgName, model string) Fielders {
	return p.fields(pkgName, model, "Get")
}

func (p *Bind) Seter(pkgName, model string) Fielders {
	return p.fields(pkgName, model, "Set")
}
